﻿/* Copyright 2010-present MongoDB Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System;

namespace MongoDB.Bson.Serialization.IdGenerators
{
    /// <summary>
    /// Represents an Id generator for Guids using the COMB algorithm.
    /// </summary>
    public class CombGuidGenerator : IIdGenerator
    {
        // private static fields
        private static CombGuidGenerator __instance = new CombGuidGenerator();

        // constructors
        /// <summary>
        /// Initializes a new instance of the CombGuidGenerator class.
        /// </summary>
        public CombGuidGenerator()
        {
        }

        // public static properties
        /// <summary>
        /// Gets an instance of CombGuidGenerator.
        /// </summary>
        public static CombGuidGenerator Instance
        {
            get { return __instance; }
        }

        // public methods
        /// <summary>
        /// Generates an Id for a document.
        /// </summary>
        /// <param name="container">The container of the document (will be a MongoCollection when called from the C# driver). </param>
        /// <param name="document">The document.</param>
        /// <returns>An Id.</returns>
        public object GenerateId(object container, object document)
        {
            var guid = Guid.NewGuid();
            var timestamp = DateTime.UtcNow;
            return NewCombGuid(guid, timestamp);
        }

        /// <summary>
        /// Tests whether an Id is empty.
        /// </summary>
        /// <param name="id">The Id.</param>
        /// <returns>True if the Id is empty.</returns>
        public bool IsEmpty(object id)
        {
            return id == null || (Guid)id == Guid.Empty;
        }

        /// <summary>
        /// Create a new CombGuid from a given Guid and timestamp.
        /// </summary>
        /// <param name="guid">The base Guid.</param>
        /// <param name="timestamp">The timestamp.</param>
        /// <returns>A new CombGuid created by combining the base Guid with the timestamp.</returns>
        public Guid NewCombGuid(Guid guid, DateTime timestamp)
        {
            // note: Guids generated by CombGuidGenerator are only considered ascending by SQL Server which compares Guids in an unusual way
            // to generate Guids considered ascending by MongoDB use the AscendingGuidGenerator

            var baseDate = new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc);
            var days = (ushort)(timestamp - baseDate).Days;
            var timeTicks = (int)(timestamp.TimeOfDay.Ticks * 300 / TimeSpan.TicksPerSecond); // convert from .NET resolution to SQL Server resolution

            // replace last 6 bytes of a new Guid with 2 bytes from days and 4 bytes from time of day
            // see: The Cost of GUIDs as Primary Keys by Jimmy Nilson
            // at: http://www.informit.com/articles/article.aspx?p=25862&seqNum=7

            var bytes = guid.ToByteArray();

            Array.Copy(BitConverter.GetBytes(days), 0, bytes, 10, 2);
            Array.Copy(BitConverter.GetBytes(timeTicks), 0, bytes, 12, 4);
            if (BitConverter.IsLittleEndian)
            {
                Array.Reverse(bytes, 10, 2);
                Array.Reverse(bytes, 12, 4);
            }

            return new Guid(bytes);
        }
    }
}
